
#include <vector>
#include <apr_date.h>
#include "../utils/StringUtils.h"
#include "../iface/APRInterface.h"
#include "../iface/testerror.h"
//#include "Actor.h"
//#include "ActorsDB.h"
#include "DBUtils.h"
#include "SQLiteTransactor.h"
#include "NetworkDB.h"

const char *NetworkDB::table_hosts_fields_names[] =
   { "hostname", "sshport", "hostfingerprint", NULL };
const char *NetworkDB::table_hosts_fields_types[] =
   { "char(128) primary key not null", "port decimal(6,0)", "char(64)", NULL };
const char *NetworkDB::table_nodes_fields_names[] =
   { "hostname", "clustername", NULL };
const char *NetworkDB::table_nodes_fields_types[] =
   { "char(128) primary key not null", "char(64)", NULL };
const char *NetworkDB::table_alias_fields_names[] =
   { "aliasname", "hostname", NULL };
const char *NetworkDB::table_alias_fields_types[] =
   { "char(128) primary key not null", "char(128)", NULL };
const char *NetworkDB::table_clusters_fields_names[] =
   { "clustername", "writedir", NULL };
const char *NetworkDB::table_clusters_fields_types[] =
   { "char(64) primary key not null", "char(32)", NULL };
const char *NetworkDB::table_special_user_credentials_fields_names[] =
   { "hostname", "username", "keyname", NULL };
const char *NetworkDB::table_special_user_credentials_fields_types[] =
   { "char(64) primary key not null", "char(64)", "char(64)", NULL };

/** Destructor for the network database. */
NetworkDB::~NetworkDB()
{
}

/** Creates the tables in the network database if they do not already
 * exist. */
void NetworkDB::createTablesIfNecessary()
{
   char *hosts_table_fields = DBUtils::makeFieldDefinitions(table_hosts_fields_names, table_hosts_fields_types);
   char *nodes_table_fields = DBUtils::makeFieldDefinitions(table_nodes_fields_names, table_nodes_fields_types);
   char *alias_table_fields = DBUtils::makeFieldDefinitions(table_alias_fields_names, table_alias_fields_types);
   char *clusters_table_fields = DBUtils::makeFieldDefinitions(table_clusters_fields_names, table_clusters_fields_types);
   ensureTablesAreCreated(4,
      "hosts", hosts_table_fields,
      "nodes", nodes_table_fields,
      "alias", alias_table_fields,
      "clusters", clusters_table_fields);
   free(hosts_table_fields);
   free(nodes_table_fields);
   free(alias_table_fields);
   free(clusters_table_fields);
}

/** Add a host to the network database. If a host fingerprint or
 * clustername is not associated with the host, then these can be
 * intentionally left blank with a NULL.
 * 
 * @param hostname The host name that is the serve as the primary key
 * to the host. Aliases can be entered into another database table and
 * can thereby be brought back to a unique identifier.
 * 
 * @param portno The port number that the host uses SSH on.
 * 
 * @param hostfingerprint The host fingerprint for this host, if
 * known. If this is not know, this field can be left blank with a
 * NULL.
 * 
 * @param clustername The cluster name to be associated with this
 * host. To be used if the head node commands for queuing and
 * management can be used effectively here.
 * 
 * @return Zero on success, non-zero on failure. */
int NetworkDB::addHost(const char *hostname, int portno,
   const char *hostfingerprint, const char *clustername)
{
   FATAL_IF_BLANK(1, hostname, "hostname");
   if (knownHost(hostname)) return 1;
   SQLiteTransactor newhost(this, "hosts", "hostname", hostname);
   newhost.setField("sshport", portno);
   newhost.setField("hostfingerprint", (hostfingerprint) ? hostfingerprint : "NULL");
   newhost.commit();
   if (clustername)
   {
      SQLiteTransactor existingcluster(this, "clusters", "clustername", clustername);
      const char *writabledir = existingcluster.getField("writedir");
      if (writabledir == NULL)
      {
         errprint("could not find cluster named '%s'", clustername);
         return 2;
      } else
      {
         SQLiteTransactor newnode(this, "nodes", "hostname", hostname);
         newnode.setField("clustername", clustername);
         newnode.commit();
      }
   }
   return 0;
}

/** Adds a cluster to the database.
 * 
 * @param clustername The name of the cluster.
 * 
 * @param writedir The directory in which to write the data and
 * perform the computations.  Can be made user specific as the tilde
 * character is appropriately expanded here.
 * 
 * @return Zero on success, non-zero on failure. */
int NetworkDB::addCluster(const char *clustername, const char *writedir)
{
   FATAL_IF_BLANK(1, clustername, "clustername");
   // Use tilde for home directory if none is specified
   const char *twritedir = ((writedir == NULL) || (strlen(writedir) == 0)) ? "~" : writedir;
   SQLiteTransactor newcluster(this, "clusters", "clustername", clustername);
   newcluster.setField("writedir", twritedir);
   newcluster.commit();
   return 0;
}

/** Add an alias to a host name.
 * 
 * @param alias An alias name for a known primary key host.
 * 
 * @param hostname A primary key host that is to be used when the
 * alias is instead specified.
 * 
 * @return Zero on success, non-zero on failure. */
int NetworkDB::addAlias(const char *alias, const char *hostname)
{
   FATAL_IF_BLANK(1, alias, "alias name");
   SQLiteTransactor existinghost(this, "nodes", "hostname", hostname);
   if (existinghost.getField("sshport") != NULL)
   {
      SQLiteTransactor newalias(this, "alias", "aliasname", alias);
      newalias.setField("hostname", hostname);
   } else
   {
      errprint("hostname '%s' does not exist!", hostname);
      return 1;
   }
   return 0;
}

/** Adds a set of user credentials enabling SSH access to a machine.
 * 
 * @param userid The user ID (actor) that represents the user. Is not
 * the special delegated SSH ID.
 * 
 * @param sshhostname The host name of the machine to be logged
 * into. This must be both the primary key for the hosts database and
 * a valid host name for the server.
 * 
 * @param sshusername The SSH user id needed to access the users
 * account on the given remote machine.
 * 
 * @param sshkeyname The name of the signing and checking keys used to
 * provide identity authentication to the remote SSH server.
 * 
 * @return Zero on success, non-zero on failure. */
int NetworkDB::addUserCredentials(const char *userid, const char *sshhostname, const char *sshusername, const char *sshkeyname)
{
   FATAL_IF_BLANK(1, userid, "user actor id");
   int retval = 0;
   char *user_credentials_table_name = StringUtils::varprintf("%s_ssh_credentials", userid);
   char *usercred_table_fields = DBUtils::makeFieldDefinitions(
      table_special_user_credentials_fields_names, table_special_user_credentials_fields_types);
   ensureTablesAreCreated(1, user_credentials_table_name, usercred_table_fields);
   SQLiteAutoLock thislock(getAutoLock());
   SQLiteTransactor specifiedhost(this, "hosts", "hostname", sshhostname);
   if (specifiedhost.getField("sshport") != NULL)
   {
      SQLiteTransactor usercredtable(this, user_credentials_table_name, "hostname", sshhostname);
      const char *olduserfield = usercredtable.getField("username");
      usercredtable.setField("username", sshusername);
      usercredtable.setField("keyname", sshkeyname);
      usercredtable.commit();
      errprint("%s credentials to '%s', for '%s@%s'",
         ((olduserfield != NULL) && (strlen(olduserfield) > 0)) ? "updating" : "adding",
         user_credentials_table_name, sshusername, sshhostname);
   } else
   {
      errprint("host specified in credential '%s@%s' does not exist!", sshusername, sshhostname);
      retval = 1;
   }
   std::vector<char *> *slist = sqlScanAllTextf("select hostname,username,keyname from '%s';", user_credentials_table_name);
   char *flattened_usercreds = StringUtils::flattenStringList(slist, ",");
   errprint("user credentials table gives - '%s'\n", flattened_usercreds);
   free(flattened_usercreds);
   StringUtils::destroyStringList(slist);
   free(usercred_table_fields);
   free(user_credentials_table_name);
   return retval;
}

/** Adds all of the SSH servers accessible to the delegated SSH user
 * ID to the specified lists.
 * 
 * @param userid The delegated SSH user ID that holds the secret keys
 * to the SSH logins.
 * 
 * @param allaccessiblehosts A pointer to a vector that on return
 * holds the primary key host names of all of the hosts that have been
 * registered to the delegated SSH user ID.
 * 
 * @param allaccessibleports A pointer to the corresponding vector of
 * ports for all the given SSH servers. */
void NetworkDB::getAllHostsAccessibleToUserID(const char *userid, std::vector<char *> *allaccessiblehosts, std::vector<int> *allaccessibleports)
{
   if ((userid == NULL) || (strlen(userid) == 0)) { errprint("user id cannot be %s", (userid) ? "empty" : "NULL"); return; }
   std::vector<char *> *allacc_host = sqlScanAllTextf("select hostname from '%s_credentials';", userid);
   // if the string list is invalid, then there are no accessible hosts. return.
   if (allacc_host == NULL) return;
   // now get the hosts and ports from the other database
   for (int i = 0; i < allacc_host->size(); i++)
   {
      int mportno = 0;
      sqlGetSingleIntf(&mportno, "select sshport from 'hosts' where hostname='%s';", allacc_host->at(i));
      if (mportno != 0)
      {
         allaccessiblehosts->push_back(allacc_host->at(i));
         allaccessibleports->push_back(mportno);
      }
      // should override with the else clause if in general
      // (non-vetted machines) use!
#ifndef VET_MACHINES
      else
      {
         allaccessiblehosts->push_back(allacc_host->at(i));
         allaccessibleports->push_back(22);
      }
#endif
   }
   delete allacc_host;
}

/** If the host is known to the system.
 * 
 * @return One if it is known (exists uniquely in the database), zero
 * if it is unknown. */
int NetworkDB::knownHost(const char *hostname)
{
   int hostcount = 0;
   sqlGetSingleIntf(&hostcount, "select count(hostname) from 'hosts' where hostname='%s';", hostname);
   return hostcount;
}

/** Get the SSH server username needed by the delegated SSH actor ID
 * to access the server with the given host name.
 *
 * @param userid The delegated SSH actor ID, owner of the SSH keys
 * that needs to access the server.
 * 
 * @param hostname The name of the host of the SSH server to which
 * access is required.
 * 
 * @return A string containing the server-specific user name on
 * success, NULL on failure. */
char *NetworkDB::getSSHUsername(const char *userid, const char *hostname)
{
   FATAL_IF_BLANK(NULL, userid, "user SSH actor id");
   FATAL_IF_BLANK(NULL, hostname, "hostname");
   char *r_username = NULL;
   sqlGetSingleStringf(&r_username, "select username from '%s_credentials' where hostname='%s';", userid, hostname);
   if (r_username == NULL)
      errprint("could not find SSH username for SSH delegate ID '%s' on host '%s'", userid, hostname);
   return r_username;
}

/** Get the key name owned by the delegated SSH actor ID needed to
 * access the server with the given host name.
 *
 * @param userid The delegated SSH actor ID, owner of the SSH keys
 * that needs to access the server.
 * 
 * @param hostname The name of the host of the SSH server to which
 * access is required.
 * 
 * @return A string containing the name of key on success, NULL on
 * failure. */
char *NetworkDB::getKeyNameNeededForHostLogin(const char *userid, const char *hostname)
{
   FATAL_IF_BLANK(NULL, userid, "user SSH actor id");
   FATAL_IF_BLANK(NULL, hostname, "hostname");
   char *r_keyname = NULL;
   sqlGetSingleStringf(&r_keyname, "select keyname from '%s_credentials' where hostname='%s';", userid, hostname);
   return r_keyname;
}

/** Updates the credentials table for the given user to allow access
 * to the specified server with a new updated key.
 *
 * @param userid The delegated SSH actor ID, owner of the SSH keys
 * that needs to access the server.
 * 
 * @param hostname The name of the host of the SSH server to which
 * access is required.
 * 
 * @param newkeyname The new key name to replace the old key
 * name. This key must be owned by the delegated SSH actor ID.
 * 
 * @return Zero on success, non-zero on failure. */
int NetworkDB::updateHostLoginToUseNewKey(const char *userid, const char *hostname, const char *newkeyname)
{
   FATAL_IF_BLANK(1, userid, "user SSH actor id");
   FATAL_IF_BLANK(1, hostname, "hostname");
   FATAL_IF_BLANK(1, newkeyname, "new key name");
   sqlInstantExecf("update '%s_credentials' set keyname='%s' where hostname='%s';", userid, newkeyname, hostname);
   return 0;
}
